package com.googlecode.recycled.xml;

import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import com.googlecode.recycled.cache.DefaultLocalCache;
import com.googlecode.recycled.lang.Cache;
import com.googlecode.recycled.lang.assertions.Assert;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.lang.UnhandledException;
import org.w3c.dom.Attr;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;

import javax.xml.namespace.QName;
import javax.xml.xpath.*;
import java.io.InputStream;
import java.util.List;

/**
 * Stateful utility class operating on XML Node using XPath syntax.
 */
public class XPaths {

    // MEMBERS

    protected XPath xpath;

    protected final Node xmlNode;

    Cache<String, XPathExpression> compiledExpressionCache = new DefaultLocalCache<String, XPathExpression>();

    // CONSTRUCTION

    public XPaths(Node xmlNode) {
        this.xmlNode = Assert.notNull(xmlNode);

        XPathFactory factory = XPathFactory.newInstance();
        xpath = factory.newXPath();
    }

    public XPaths(InputStream xml) {
        this(DocumentBuilder.newDocument(xml));
    }

    // MEMBERS HELPERS

    public Node getXmlNode() {
        return xmlNode;
    }

    // EVALUATION HELPERS

    protected Object doEvaluate(String path, QName type) {
        try {
            XPathExpression xPathExpression = compiledExpressionCache.get(path);
            if (xPathExpression == null) {
                xPathExpression = xpath.compile(path);
                compiledExpressionCache.put(path, xPathExpression);
            }
            return xPathExpression.evaluate(xmlNode, type);
        } catch (XPathExpressionException e) {
            throw new UnhandledException(e);
        }
    }

    // NODE METHODS

    public boolean exists(String path) {
        return doEvaluate(path, XPathConstants.NODE) != null;
    }

    public Node node(String path) {
        return (Node) doEvaluate(Assert.notBlank(path), XPathConstants.NODE);
    }

    public Iterable<Node> nodes(String path) {
        return Nodes.asList((NodeList) doEvaluate(path, XPathConstants.NODESET));
    }

    public Node assertNode(String path) {
        Node node = node(path);
        if (node == null) {
            int indexOfSeparator = path.lastIndexOf('/');
            if (indexOfSeparator == 0) {
                return Elements.addElement(xmlNode, path.substring(1));
            }
            Node parent = assertNode(path.substring(0, indexOfSeparator));
            String childName = path.substring(path.lastIndexOf('/') + 1);
            if (childName.charAt(0) == '@') {
                childName = childName.substring(1);
                Element parentElement = (Element) parent;
                parentElement.setAttribute(childName, StringUtils.EMPTY);
            } else {
                node = Elements.addElement(parent, childName);
            }
        }
        return node;
    }

    public void replaceNode(String path, Node node) {
        Node parentNode = assertNode(path).getParentNode();
        parentNode.removeChild(node);
        Nodes.addNode(parentNode, node);
    }

    public void addNode(String path, Node node) {
        Nodes.addNode(assertNode(path), node);
    }

    public void remove(String path) {
        List<Node> targetNodes = Lists.newLinkedList(Assert.notEmpty(nodes(path)));
        for (Node targetNode : targetNodes) {
            if (Nodes.isAttribute(targetNode)) {
                Attr targetAttribute = (Attr) targetNode;
                Assert.notNull(targetAttribute.getOwnerElement().removeAttributeNode(targetAttribute));
            } else {
                Assert.notNull(targetNode.getParentNode().removeChild(targetNode));
            }
        }
    }

    // ELEMENT METHODS

    public Element element(String path) {
        return (Element) node(path);
    }

    public Iterable<Element> elements(String path) {
        return Nodes.asElements((NodeList) doEvaluate(path, XPathConstants.NODESET));
    }

    // STRING METHODS

    public String string(String path) {
        try {
            return node(path).getTextContent();
        } catch (NullPointerException npe) {
            return null;
        }
    }

    public Iterable<String> strings(String path) {
        return Iterables.transform(nodes(path), new Nodes.NodeToTextFunction());
    }

    public void setString(String path, String value) {
        Nodes.setText(assertNode(path), value);
    }

    public void addString(String path, String value) {
        Elements.addElement(assertNode(path), value);
    }

}
